package models

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5/middleware"
)

// ProblemContentType is the default content type for a Problem response
const ProblemContentType = "application/problem+json"

// ProblemType contains a URI that identifies the problem type. This URI will,
// ideally, contain human-readable documentation for the problem when
// de-referenced.
type ProblemType string

const (
	// ProblemTypeDefault is the default problem type.
	ProblemTypeDefault = ProblemType("about:blank")
	// TODO: add more problem types that can be used to link to docs.
	// ...
)

// Problem is the RFC 7807 response body.
type Problem interface {
	Respond(w http.ResponseWriter)
	Error() string
	ProblemType() ProblemType
	ProblemTitle() string
	ProblemStatus() int
}

// StatusProblem is the RFC 7807 response body without additional fields.
type StatusProblem struct {
	Err error `json:"-"` // low-level runtime error

	// Type is a URI reference that identifies the problem type.
	Type ProblemType `json:"type"`
	// Title is a short, human-readable summary of the problem type.
	Title string `json:"title"`
	// Status is the HTTP status code generated by the origin server for this occurrence of the problem.
	Status int `json:"status"`
	// Detail is a human-readable explanation specific to this occurrence of the problem.
	Detail string `json:"detail,omitempty"`
	// Instance is a URI reference that identifies the specific occurrence of the problem.
	Instance string `json:"instance,omitempty"`

	// Extension fields are allowed by spec, see https://datatracker.ietf.org/doc/html/rfc7807#section-3.2
	Extensions map[string]interface{} `json:"extensions,omitempty"`
}

var _ Problem = (*StatusProblem)(nil)

func (p *StatusProblem) Error() string {
	if p.Err == nil {
		return fmt.Sprintf("[%s] %s", p.Title, p.Detail)
	}

	return fmt.Sprintf("[%s] %s - %s", p.Title, p.Err.Error(), p.Detail)
}

func (p *StatusProblem) RawError() error {
	return p.Err
}

func (p *StatusProblem) ProblemType() ProblemType {
	return p.Type
}

func (p *StatusProblem) ProblemStatus() int {
	return p.Status
}

func (p *StatusProblem) ProblemTitle() string {
	return p.Title
}

// Respond will render the problem as JSON to the provided ResponseWriter.
func (p *StatusProblem) Respond(w http.ResponseWriter) {
	RespondProblem(p, w)
}

// Respond will render the problem as JSON to the provided ResponseWriter.
func RespondProblem(problem Problem, w http.ResponseWriter) {
	// Respond
	buf := &bytes.Buffer{}
	enc := json.NewEncoder(buf)
	enc.SetEscapeHTML(true)
	_ = enc.Encode(problem)

	w.Header().Set("Content-Type", ProblemContentType)
	w.WriteHeader(problem.ProblemStatus())
	_, _ = w.Write(buf.Bytes()) // nosemgrep
}

// NewStatusProblem will generate a problem for the provided HTTP status
// code. The Problem's Status field will be set to match the status argument,
// and the Title will be set to the default Go status text for that code.
func NewStatusProblem(ctx context.Context, err error, status int) *StatusProblem {
	var instance string
	reqID := middleware.GetReqID(ctx)
	if reqID != "" {
		instance = fmt.Sprintf("urn:request:%s", reqID)
	}

	// Set context canceled errors to 408.
	// Context canceled errors either happen when the client cancels the request or when a timeout happens in dependency.
	// If client cancels the request, the status code doesn't matter, because the client will not see the response.
	// If timeout happens in dependency, we want to return 408 to the client.
	if err != nil && strings.Contains(err.Error(), "context canceled") {
		status = http.StatusRequestTimeout
	}

	var detail string
	if err != nil {
		detail = err.Error()
	}

	if status == http.StatusInternalServerError {
		detail = ""
	}

	return &StatusProblem{
		Err:        err,
		Type:       ProblemTypeDefault,
		Title:      http.StatusText(status),
		Status:     status,
		Detail:     detail,
		Instance:   instance,
		Extensions: map[string]interface{}{},
	}
}
